package net.badcurrent.ldapodm;

import org.springframework.ldap.LdapTemplate;
import org.springframework.ldap.support.DistinguishedName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.lang.StringUtils;

import java.util.*;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;

import net.badcurrent.ldapodm.ldap.EntryAttributesMapper;
import net.badcurrent.ldapodm.ldap.DistinguishedNameFactory;
import net.badcurrent.ldapodm.ldap.PassThroughAttributesMapper;
import net.badcurrent.ldapodm.typeconverter.TypeConverter;
import net.badcurrent.ldapodm.config.ClassMetaData;

import javax.naming.directory.*;
import javax.naming.NamingException;

public class SessionImpl implements Session, SessionImplementor {

    private static Log log = LogFactory.getLog(SessionImpl.class);

    private LdapTemplate ldapTemplate;
    private Map<Class, EntryAttributesMapper> typeMappers = new HashMap<Class, EntryAttributesMapper>();
    private Map<Class, TypeConverter> typeConverters = new HashMap<Class, TypeConverter>();
    private Map<Class, DistinguishedNameFactory> distinguishedNameFactories =
            new HashMap<Class, DistinguishedNameFactory>();
    private String baseDn;
    private Map<DistinguishedName, UnitOfWork> identityMap;
    private Map<Class, ClassMetaData> classMetaDataMap;
    private Map<Object, DistinguishedName> objectToNameMap = new HashMap<Object, DistinguishedName>();
    private boolean isClosed;

    private static ThreadLocal<SessionImplementor> sessionHolder
            = new ThreadLocal<SessionImplementor>() {
        public SessionImplementor initialValue(SessionImplementor session) {
            return session;
        }
    };

    public SessionImpl() {
        identityMap = new HashMap<DistinguishedName, UnitOfWork>();
        sessionHolder.set(this);
        isClosed = false;
    }

    public SessionImpl(LdapTemplate ldapTemplate, Map<Class, EntryAttributesMapper> typeMappers,
                       Map<Class, DistinguishedNameFactory> distinguishedNameFactories,
                       Map<Class, TypeConverter> typeConverters,
                       Map<Class, ClassMetaData> classMetaDataMap,
                       String baseDn) {
        this.ldapTemplate = ldapTemplate;
        this.typeMappers = typeMappers;
        this.distinguishedNameFactories = distinguishedNameFactories;
        this.baseDn = baseDn;
        this.typeConverters = typeConverters;
        identityMap = new HashMap<DistinguishedName, UnitOfWork>();
        this.classMetaDataMap = classMetaDataMap;
        sessionHolder.set(this);
    }

    public void setBaseDn(String baseDn) {
        this.baseDn = baseDn;
    }

    public void setClassMetaDataMap(Map<Class, ClassMetaData> classMetaDataMap) {
        this.classMetaDataMap = classMetaDataMap;
    }

    public void setDistinguishedNameFactories(Map<Class, DistinguishedNameFactory> distinguishedNameFactories) {
        this.distinguishedNameFactories = distinguishedNameFactories;
    }

    public void setIdentityMap(Map<DistinguishedName, UnitOfWork> identityMap) {
        this.identityMap = identityMap;
    }

    public void setLdapTemplate(LdapTemplate ldapTemplate) {
        this.ldapTemplate = ldapTemplate;
    }

    public void setTypeConverters(Map<Class, TypeConverter> typeConverters) {
        this.typeConverters = typeConverters;
    }

    public void setTypeMappers(Map<Class, EntryAttributesMapper> typeMappers) {
        this.typeMappers = typeMappers;
    }

    public static SessionImplementor getThreadSession() {
        return sessionHolder.get();
    }

    public static void setThreadSession(SessionImplementor session) {
        sessionHolder.set(session);
    }

    public Object load(String distinguishedName, Class mappedClass) {
        if (log.isDebugEnabled()) log.debug("Loading class: " + mappedClass + " with dn: " + distinguishedName);
        DistinguishedName dn = new DistinguishedName(distinguishedName);
        if (identityMap.containsKey(new DistinguishedName(distinguishedName))) {
            if (log.isDebugEnabled()) log.debug("Loading from identityMap: " + distinguishedName);
            UnitOfWork uow = identityMap.get(new DistinguishedName(distinguishedName));
            return uow.getObject();
        }

        String structuralObjectClass = classMetaDataMap.get(mappedClass).getStructuralObjectClass();
        List results = ldapTemplate.search(dn, "(objectClass=" + structuralObjectClass + ")",
                SearchControls.OBJECT_SCOPE, typeMappers.get(mappedClass));
        if (results.size() == 0) {
            if (log.isDebugEnabled()) log.debug("No results found for: " + distinguishedName);
            return null;
        }
        Object o = results.get(0);

        if (log.isDebugEnabled()) log.debug("Adding to identityMap: " + distinguishedName);
        identityMap.put(new DistinguishedName(distinguishedName), new UnitOfWork(o, false));

        return o;
    }

    @SuppressWarnings({"unchecked"})
    public List find(String queryString, Class mappedClass) {
        if (log.isDebugEnabled()) log.debug("Querying for: " + mappedClass + " with query: "
                + queryString);

        List results = new ArrayList();

        List<Attributes> ldapResults = (List<Attributes>) ldapTemplate.search("", queryString, SearchControls.SUBTREE_SCOPE,
                new PassThroughAttributesMapper());
        EntryAttributesMapper mapper = typeMappers.get(mappedClass);
        DistinguishedNameFactory dnFactory = distinguishedNameFactories.get(mappedClass);
        for (Attributes attrs : ldapResults) {
            DistinguishedName dn = dnFactory.generateDistinguishedNameFromAttributes(attrs);
            if (identityMap.containsKey(new DistinguishedName(dn))) {
                if (log.isDebugEnabled()) log.debug("Result found in identityMap for: " + dn);
                results.add(identityMap.get(new DistinguishedName(dn)).getObject());
            } else {
                try {
                    Object o = mapper.mapFromAttributes(attrs);
                    if (log.isDebugEnabled()) log.debug("Adding to identityMap: " + dn);
                    identityMap.put(dn, new UnitOfWork(o, false));
                    results.add(o);
                } catch (NamingException e) {
                    throw new net.badcurrent.ldapodm.NamingException(e);
                }
            }
        }
        return results;
    }

    public void detach(Object o) {
        if (log.isDebugEnabled()) log.debug("Attemping to remove from identityMap: " + o);
        boolean objectRemoved = false;
        for (DistinguishedName key : identityMap.keySet()) {
            if (identityMap.get(key).getObject() == o) {
                identityMap.remove(key);
                objectRemoved = true;
                if (log.isDebugEnabled()) log.debug("Object removed from identityMap: " + o);
                break;
            }
        }
        if (!objectRemoved) {
            log.warn("Object not found: " + o);
        }
    }

    public void flush() {
        if (log.isDebugEnabled()) log.debug("Flushing Session to directory");
        for (DistinguishedName dn : identityMap.keySet()) {
            if (log.isDebugEnabled()) log.debug("Flushing: " + dn);
            UnitOfWork uow = identityMap.get(dn);

            Object o = identityMap.get(dn).getObject();
            DistinguishedName currentDn =
                    resolveDnFactory(o.getClass()).generateDistinguishedNameFromInstance(o);

            if (uow.isNew()) {
                if (log.isDebugEnabled()) log.debug("Binding: " + dn);
                update(uow.getObject());
                ldapTemplate.bind(currentDn, null, uow.getAttributes());
            } else {
                if (uow.getModificationItems() != null) {
                    if (currentDn.toString().compareTo(dn.toString()) != 0) {
                        if (log.isDebugEnabled()) log.debug("Renaming and modifying: " + dn);
                        ldapTemplate.rename(dn, currentDn);
                        ldapTemplate.modifyAttributes(currentDn, identityMap.get(dn).getModificationItems());
                    } else {
                        if (log.isDebugEnabled()) log.debug("Modifying: " + dn);
                        ldapTemplate.modifyAttributes(dn, identityMap.get(dn).getModificationItems());
                    }
                }
            }
        }
    }

    protected void update(Object o) {
        if (log.isDebugEnabled()) log.debug("Saving object in session: " + o);

        UnitOfWork uow = identityMap.get(objectToNameMap.get(o));
        ClassMetaData metaData = resolveClassMetaData(o.getClass());

        Attributes attrs = new BasicAttributes(true);

        for (String propertyName : metaData.getProperties()) {
            Method getter;

            try {
                getter = o.getClass().getMethod("get" + StringUtils.capitalize(propertyName));
                Object result = getter.invoke(o);
                if (result == null) continue;
                for (String attrName : metaData.getAttributesForProperty(propertyName)) {
                    if (result instanceof String) {
                        attrs.put(new BasicAttribute(attrName, result));
                    } else {
                        TypeConverter typeConverter = typeConverters.get(result.getClass());
                        if (typeConverter == null) {
                            log.error("No TypeConverter registered for: " + result.getClass());
                            throw new MappingException("No TypeConverter registered for: "
                                    + result.getClass());
                        }
                        attrs.put(new BasicAttribute(attrName, typeConverter.toString(result)));
                    }
                }
                uow.addAttributes(propertyName, attrs);

            } catch (NoSuchMethodException e) {
                throw new MappingException(e);
            } catch (IllegalAccessException e) {
                throw new MappingException(e);
            } catch (InvocationTargetException e) {
                throw new MappingException(e);
            }

        }

        /* try resolving class key by class and superclass in case o is a cglib proxy (not sure if this is the
        best way to do that...)*/
        DistinguishedNameFactory dnf = distinguishedNameFactories.get(o.getClass());
        if (dnf == null)
            dnf = distinguishedNameFactories.get(o.getClass().getSuperclass());
        if (dnf == null)
            throw new MappingException("No DistinguishedNameFactory found for: " + o);
        identityMap.put(dnf.generateDistinguishedNameFromInstance(o), uow);
    }


    public void save(Object o) {
        if (log.isDebugEnabled()) log.debug("Saving object in session: " + o);

        if (isObjectAttached(o)) {
            log.warn("Object is already attached, calling save() is not necessary: " + o);
            return;
        }

        UnitOfWork uow = new UnitOfWork(o, true);
        ClassMetaData metaData = resolveClassMetaData(o.getClass());

        uow.setStructuralObjectClass(metaData.getStructuralObjectClass());
        for (String objectClass : metaData.getObjectClasses()) {
            uow.addObjectClass(objectClass);
        }

        Attributes attrs = new BasicAttributes(true);

        for (String propertyName : metaData.getProperties()) {
            Method getter;

            try {
                getter = o.getClass().getMethod("get" + StringUtils.capitalize(propertyName));
                Object result = getter.invoke(o);
                if (result == null) continue;
                for (String attrName : metaData.getAttributesForProperty(propertyName)) {
                    if (result instanceof String) {
                        attrs.put(new BasicAttribute(attrName, result));
                    } else {
                        TypeConverter typeConverter = typeConverters.get(result.getClass());
                        if (typeConverter == null) {
                            log.error("No TypeConverter registered for: " + result.getClass());
                            throw new MappingException("No TypeConverter registered for: "
                                    + result.getClass());
                        }
                        attrs.put(new BasicAttribute(attrName, typeConverter.toString(result)));
                    }
                }
                uow.addAttributes(propertyName, attrs);

            } catch (NoSuchMethodException e) {
                throw new MappingException(e);
            } catch (IllegalAccessException e) {
                throw new MappingException(e);
            } catch (InvocationTargetException e) {
                throw new MappingException(e);
            }

        }

        /* try resolving class key by class and superclass in case o is a cglib proxy (not sure if this is the
        best way to do that...)*/
        DistinguishedNameFactory dnf = distinguishedNameFactories.get(o.getClass());
        if (dnf == null)
            dnf = distinguishedNameFactories.get(o.getClass().getSuperclass());
        if (dnf == null)
            throw new MappingException("No DistinguishedNameFactory found for: " + o);
        identityMap.put(dnf.generateDistinguishedNameFromInstance(o), uow);
        objectToNameMap.put(o, dnf.generateDistinguishedNameFromInstance(o));
    }

    public boolean isObjectAttached(Object o) {
        if (log.isDebugEnabled()) log.debug("Checking if object is attached to the Session: " + o);
        boolean objectAttached = false;
        for (UnitOfWork uow : identityMap.values()) {
            if (o == uow.getObject()) {
                objectAttached = true;
                break;
            }
        }
        return objectAttached;
    }

    @SuppressWarnings({"unchecked"})
    public Collection<String> getAttributesForProperty(Class clazz, String propertyName) {
        return resolveClassMetaData(clazz).getAttributesForProperty(propertyName);
    }

    public TypeConverter getTypeConverter(Class clazz) {
        return typeConverters.get(clazz);
    }

    public void updateUnitOfWork(Object o, String propertyName, Attributes attrs) {
        boolean updated = false;
        for (DistinguishedName dn : identityMap.keySet()) {
            if (o == identityMap.get(dn).getObject()) {
                identityMap.get(dn).addAttributes(propertyName, attrs);
                updated = true;
                break;
            }
        }
        if (!updated) {
            throw new SessionException("No Unit of Work present for: " + o);
        }
    }

    public boolean isLdapProperty(Class clazz, String propertyName) {
        return resolveClassMetaData(clazz).getAttributesForProperty(propertyName) != null;
    }

    public void close() {
        isClosed = true;
    }

    private ClassMetaData resolveClassMetaData(Class clazz) {
        ClassMetaData classMetaData;
        classMetaData = classMetaDataMap.get(clazz);
        if (classMetaData == null)
            classMetaData = classMetaDataMap.get(clazz.getSuperclass());
        if (classMetaData == null)
            throw new MappingException("Class not recognized: " + clazz);
        return classMetaData;
    }

    private DistinguishedNameFactory resolveDnFactory(Class clazz) {
        DistinguishedNameFactory dnFactory;
        dnFactory = distinguishedNameFactories.get(clazz);
        if (dnFactory == null)
            dnFactory = distinguishedNameFactories.get(clazz.getSuperclass());
        if (dnFactory == null)
            throw new MappingException("Class not recognized: " + clazz);
        return dnFactory;
    }

}
